/**
 * dbms-sjtu
 * MasterTable.java created at Aug 13, 2011
 */
package dbms.metadata;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import dbms.Constants;
import dbms.buffer.Buffer;
import dbms.disk.Page;
import dbms.disk.PageFormatter;
import dbms.record.FieldInfo;
import dbms.record.RecordPage;
import dbms.record.Schema;
import dbms.record.TableInfo;
import dbms.record.Record;
import dbms.server.DbmsServer;

/**
 * @author weapon
 */
public class MasterPage implements PageFormatter{
	/*
	 * some global variables, write them to the head of database file
	 */
	public static final String VERSION = "SQLsjk dbms v1.0";
	/**
	 * default is 1024, it can change by user. page 1 head is 100bytes long
	 */
	public static int pageSize = Constants.BLOCKSIZE;
	public static int freeListPageNum = 0;
	public static int freePageCount = 0;
	public static int schemaVersion = 0;
	public static int autoVacuum = 0;
	
	/**
	 * key: tablename, value: RecordPage
	 */
	public Map<String, RecordPage> tableMap = new HashMap<String, RecordPage>();
	public Map<String, ViewInfo> viewMap = new HashMap<String, ViewInfo>();
	public RecordPage recordpage;
	public TableInfo tableMaster;
	
	public MasterPage(){};
	
	public MasterPage(Buffer buffer, boolean empty){
		this.tableMaster = createMasterTable();
		this.recordpage = new RecordPage(buffer, tableMaster, 1);
		if(empty){
			//format the master page, write file header
			this.format(this.recordpage.buffer.page);
		} else {
			/**
			 * init the record offset of all tables
			 */
			//this.recordpage.initRecordOffsetsMap();
			/**
			 * init the table map
			 * key: tablename, value: tableinfo
			 */
			this.initTableMap();
		}
	}
	
	
	/**
	 * Record : rowId dataByteNum  recHeadSize type1 type2 type3 ... typeN data1 data2 data3 ... dataN
	 */
	public void initTableMap(){
		freeListPageNum = this.recordpage.buffer.getInt(20);
		int pageNum = freeListPageNum;
		Buffer buffer;
		while(pageNum != 0){
			if((buffer = DbmsServer.bufferManager.findBufferByBlock(pageNum)) == null){
				buffer = DbmsServer.bufferManager.allocBuffer();
				buffer.connectToBlock(pageNum);
			}
			DbmsServer.freePageList.add(pageNum);
			pageNum = buffer.getInt(Constants.BLOCKSIZE - Constants.INT_SIZE);
		}
		
		
		int recordNum = this.recordpage.buffer.getInt(0 + Constants.FILE_HEADER);
		//int firstRecordOffset = this.recordpage.buffer.getInt(1 * Constants.INT_SIZE + Constants.FILE_HEADER);
		//int freeByteNum = this.recordpage.buffer.getInt(2 * Constants.INT_SIZE + Constants.FILE_HEADER);
		//int firstFreeRecOffset = this.recordpage.buffer.getInt(3 * Constants.INT_SIZE + Constants.FILE_HEADER);
		if(recordNum != 0){
			int offset, headSize, column;
			int[] type;
			Object[] data = null;
			/**
			 * the general fetch data style
			 */
			for(int i = 0; i < recordNum; i++){
				String tableName, SQL;
				int pos = 0, rootpage; //record the offset of each data
				offset = this.recordpage.buffer.getInt((4 + i) * Constants.INT_SIZE + Constants.FILE_HEADER);
				if(offset == Integer.MAX_VALUE){
					continue;
				}
				headSize = this.recordpage.buffer.getInt(offset + 2 * Constants.INT_SIZE);
				column = headSize / Constants.INT_SIZE;
				type = new int[column];
				data = new Object[column];
				for(int j = 0; j < column; j++){
					data[j] = new Object();
					type[j] = this.recordpage.buffer.getInt(offset + (3 + j) * Constants.INT_SIZE);
					/**
					 * type 
					 * 0       null
					 * 4      integer
					 * N > 5  varchar (N-5)/2
					 */
					if(type[j] == 0){
						pos += 0;
					} else if(type[j] == Constants.INTEGER){
						data[j] = this.recordpage.buffer.getInt(offset + (3+column) * Constants.INT_SIZE + pos);
						pos += Constants.INT_SIZE;
					} else {
						data[j] = this.recordpage.buffer.getString((offset + (3+column) * Constants.INT_SIZE + pos), (type[j]-5)/2);
						pos += (type[j] - 5)/2;
					}
				}
				
				tableName = (String)data[1];
				rootpage = (Integer)data[3];
				SQL = (String)data[4];
				
				if(((Integer)data[0]) == 0){
					TableInfo newTable = new TableInfo(tableName, this.transSQLToSchema(SQL));
					newTable.setFirstPage(rootpage);
					newTable.setRowId(i + 1);
					
					//init the primary key index
					String primaryKeyField = null;
					if((primaryKeyField = newTable.getTableSchema().primaryKeyField) != null){
						Buffer buffer1 = null;
						if((buffer1 = DbmsServer.bufferManager.findBufferByBlock(newTable.getFirstPage())) == null){
							buffer1 = DbmsServer.bufferManager.allocBuffer();
							buffer1.connectToBlock(newTable.getFirstPage());
						}
						RecordPage recordPage = new RecordPage(buffer1, newTable, 0);
						List<String> fieldList = new ArrayList<String>();
						fieldList.add(primaryKeyField);
						Map<Integer, List<Object>> primaryKeyData = recordPage.select(fieldList);
						
						for(Integer integer : primaryKeyData.keySet()){
							newTable.primaryKeyIndex.add(primaryKeyData.get(integer).get(0));
						}
					}
					
					
					
					//buffer: null   
					tableMap.put(tableName, new RecordPage(null, newTable, 0));
				} else if(((Integer)data[0]) == 1){
					ViewInfo viewInfo = new ViewInfo(SQL, i + 1);
					viewMap.put(tableName, viewInfo);
				} else {
					//FIXME
				}
				
			}
			
		}
	}
	
	/*
	 * @see dbms.disk.PageFormatter#format(dbms.disk.Page)
	 */
	@Override
	public void format(Page p) {
		p.setString(0, VERSION);
		p.setInt(16, pageSize);
		p.setInt(20, freeListPageNum);
		p.setInt(24, freePageCount);
		p.setInt(28, schemaVersion);
		p.setInt(32, autoVacuum);
	}
	
	public TableInfo createMasterTable(){
		Schema schema = new Schema();
		/**
		 * 0 table
		 * 1 view
		 * 2 index
		 * 3 trigger
		 */
		schema.addIntField("type");
		schema.addStrField("name", 20);
		schema.addStrField("tbl_name", 20);
		schema.addIntField("rootpage");
		schema.addStrField("SQL", 200);
		TableInfo master = new TableInfo(Constants.MASTER_TABLE_NAME, schema);
		/**
		 * sqlsjk_master is at page 1 of database file
		 */
		master.setFirstPage(Constants.MASTER_BLOCK);
		
		return master;
	}
	
	/**
	 * translate SQL statement to table Schema
	 * @param SQL : sql statement stores in database file
	 * @return table schema
	 */
	public Schema transSQLToSchema(String SQL){
		Schema result = new Schema();
		String schema = SQL.substring(SQL.indexOf("(") + 1, SQL.lastIndexOf(")"));
		String[] fieldString = schema.split(",");
		for(int i = 0; i < fieldString.length; i++){
			// id INTEGER, name VARCHAR(10), address CHAR(1), model DOUBLE, price DECIMAL(3,2) 
			String[] temp = fieldString[i].trim().split(" ");
			String fieldName = temp[0];
			int type, length;
			if(temp[1].contains("INT")){
				type = Constants.INTEGER;
				length = Constants.INT_SIZE;
			} else if(temp[1].contains("CHAR")){
				type = Constants.CHAR;
				length = Integer.parseInt(temp[1].substring(temp[1].indexOf("(") + 1, temp[1].indexOf(")")));
			} else if(temp[1].contains("DOUBLE")){
				type = Constants.DOUBLE;
				length = Constants.INT_SIZE;  // in fact, it is float
			} else if(temp[1].contains("DECIMAL")){
				type = Constants.DECIMAL;
				int dotBefore = Integer.parseInt(temp[1].substring(temp[1].indexOf("(") + 1, temp[1].indexOf(",")));
				int dotAfter = Integer.parseInt(temp[1].substring(temp[1].indexOf(",") + 1, temp[1].indexOf(")")));
				length = dotBefore << 16 + dotAfter;
			} else {
				type = Constants.VARCHAR;
				length = Integer.parseInt(temp[1].substring(temp[1].indexOf("(") + 1, temp[1].indexOf(")")));
			}
			if(temp.length >= 3){
				result.addField(fieldName, new FieldInfo(type, length, FieldInfo.PRIMARYKEY));
				result.primaryKeyField = fieldName;
			} else {
				result.addField(fieldName, new FieldInfo(type, length, FieldInfo.USUAL));
			}
		}
		return result;
	}

	
	/**
	 * generate a record by the metadata tableinfo
	 * @param rowid : the row id of this table in sqlsjk_master
	 * @param table : the table object
	 * @return a record at sqlsjk_master
	 */
	public Record transTableToRecord(int rowid, TableInfo table){
		List<Object> data = new ArrayList<Object>();
		data.add(0);  // type
		data.add(table.getTableName()); // name
		data.add(table.getTableName());   // tbl_name
		data.add(table.getFirstPage());  // rootpage
		data.add(transSQLBySchema(table.getTableName(), table.getTableSchema())); // SQL
		return new Record(rowid, data);
	}
	
	/**
	 * generate a SQL statement by a TableInfo metadata
	 * @param tableName
	 * @param schema
	 * @return the SQL statement
	 */
	public String transSQLBySchema(String tableName, Schema schema){
		String sql = "CREATE TABLE " + tableName + "(";
		int rank = 0;
		int primaryKey = 0;
		String fieldString = null;
		while(rank != schema.getFieldRank().size()){
			for(String field : schema.getFieldRank().keySet()){
				if(schema.getFieldRank().get(field) == rank){
					fieldString = field; 
					break;
				}
			}
			
			if(schema.getInfo().get(fieldString).getType() == Constants.INTEGER){
				sql += fieldString + " INT";
			} else if(schema.getInfo().get(fieldString).getType() == Constants.CHAR){
				sql += fieldString + " CHAR(" + schema.getInfo().get(fieldString).getLength() + ")";
			} else if(schema.getInfo().get(fieldString).getType() == Constants.DECIMAL){
				int length = schema.getInfo().get(fieldString).getLength();
				sql += fieldString + " DECIMAL(" + length/(1<<16) + "," + length%(1<<16) + ")";
			} else if(schema.getInfo().get(fieldString).getType() == Constants.DOUBLE){
				sql += fieldString + " DOUBLE";
			} else {
				sql += fieldString + " VARCHAR(" + schema.getInfo().get(fieldString).getLength() + ")";
			}
			if(primaryKey == 0){
				if(schema.getInfo().get(fieldString).getKey() == 2){
					sql += " PRIMARY KEY";
					primaryKey = 1;
				}
			}
			rank++;
			if(rank != schema.getFieldRank().size()){
				sql += ",";
			}
		}
		sql += ")";
		return sql;
	}
	
	/**
	 * insert a new table into sqlsjk_master, then write it to database file<br>
	 * if this is the first time to insert, you should init this page<br>
	 * int recordNum;                   0<br>
	 * int firstRecordOffset;			1<br>
	 * int freeByteNum;					2<br>
	 * int firstFreeRecOffset;			3<br>
	 * @param table
	 */
	public void insert(TableInfo table){
		int rowid = this.recordpage.recordNum + 1;
		table.setRowId(rowid);
		this.tableMap.put(table.getTableName(), new RecordPage(null, table, 0));
		this.recordpage.insert(transTableToRecord(rowid, table));
	}
	
	
	
	/**
	 * insert a new view into sqlsjk_master, then write it to database file.
	 * @param viewRecord
	 */
	public void insert(String viewName, String SQL){
		int rowid = this.recordpage.recordNum + 1;
		//String SQL = transSQLBySelectExp(viewName, selectExp);
		List<Object> recordData = new ArrayList<Object>();
		recordData.add(1); //view
		recordData.add(viewName); //name
		recordData.add(viewName); //tbl_name
		recordData.add(0);   // rootpage null
		recordData.add(SQL);  //SQL statement
		
		this.viewMap.put(viewName, new ViewInfo(SQL, rowid));
		this.recordpage.insert(new Record(rowid, recordData));
	}
	
	/**
	 * 
	 * @param index
	 */
	public void delete(int index){
		this.recordpage.delete(index);
	}
	
	public void update(TableInfo table){
		//TODO
	}
}
